{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "f52c463e",
   "metadata": {},
   "source": [
    "# Домашнее задание 9\n",
    "\n",
    "## Моделирование работы системы массового обслуживания\n",
    "### Цель и результат\n",
    "\n",
    "Научиться создавать массивы псевдослучайных чисел с нужным распределением и познакомиться с основами компьютерного моделирования работы систем массового обслуживания.\n",
    "\n",
    "### Задание\n",
    "\n",
    "Есть парикмахерская, с 𝑛 парикмахерами и очередью. Предполагается, что поток клиентов,\n",
    "желающих постричься – пуассоновский с интенсивностью 𝜆, поток постриженных 𝑖-м\n",
    "парикмахером клиентов – пуассоновский с интенсивностью 𝜇𝑖\n",
    "\n",
    "1. Выбрав значения 𝑛, 𝜆 и 𝜇𝑖, написать программу, моделирующую работу этой парикмахерской в течение рабочего дня с 10:00 до 20:00. Результатом работы программы может быть текстовый файл, каждая строка которого отражает событие в работе парикмахерской: появление нового клиента или окончание стрижки клиента. Файл может выглядеть примерно так (нули и единицы перед слешем показывают, какой из парикмахеров занят, а какой свободен; число после слеша показывает число клиентов в очереди):\n",
    "\n",
    "  10:03 - 0 0 1 / 0\n",
    "  \n",
    "  10:11 - 1 0 1 / 0\n",
    "  \n",
    "  10:18 - 1 1 1 / 0\n",
    "  \n",
    "  10:24 - 1 1 0 / 0\n",
    "  \n",
    "  10:27 - 1 1 1 / 0\n",
    "  \n",
    "  10:34 - 1 1 1 / 1\n",
    "  \n",
    "  10:39 - 1 1 1 / 2\n",
    "  \n",
    "  10:42 - 1 1 1 / 1\n",
    "\n",
    "\n",
    "2. Подобрать значения 𝑛, 𝜆 и 𝜇𝑖 так, чтобы результаты работы парикмахерской выглядели правдоподобно, т. е. чтобы не образовывалась гигантская очередь, стрижка не занимала бы 1-2 минуты и т.п.\n",
    "\n",
    "3. Написать словами свои соображения о том, как должны соотноситься друг с другом интенсивность потока клиентов и суммарная интенсивность работы парикмахеров, чтобы парикмахерская работала оптимальным образом (с одной стороны без многолюдных очередей, с другой – без постоянного простоя парикмахеров)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "df467558",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from scipy import stats\n",
    "from enum import Enum\n",
    "from datetime import datetime, timedelta\n",
    "from queue import Queue\n",
    "from tqdm.notebook  import tqdm"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "8ea6b1ca",
   "metadata": {},
   "outputs": [],
   "source": [
    "class NodeState(Enum):\n",
    "    \"\"\"Статусы узлов обслуживания\"\"\"\n",
    "    # Свободен\n",
    "    FREE = 0\n",
    "    # Занят\n",
    "    BUSY = 1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "ae990bce",
   "metadata": {},
   "outputs": [],
   "source": [
    "class PoissonServiceNode:\n",
    "    \"\"\" Узел обслуживания Пуассоновского процесса\n",
    "    \n",
    "    Параметры\n",
    "    ---------\n",
    "    m : float\n",
    "      Интенсивность обслуживания\n",
    "    t : int\n",
    "      Интервал времени интенсивности обслуживания\n",
    "    exp_scale_procent : float\n",
    "      Стандартное отклонение для генерации случайного значения времени занятости,\n",
    "      которое расчитывается по формуле: exp_scale_procent * (t / m)\n",
    "    \"\"\"\n",
    "    def __init__(self, m=1.0, t=60, exp_scale_procent=0.1):\n",
    "        \n",
    "        assert 0 <= exp_scale_procent <= 1\n",
    "        \n",
    "        self.m = m\n",
    "        self.t = t\n",
    "        \n",
    "        self.exp_scale_procent = exp_scale_procent\n",
    "        self.__state = NodeState.FREE\n",
    "        \n",
    "        # Если нода занята, то в данной переменной храним время, через которое\n",
    "        # узел будет свободен после момента взятия заявки\n",
    "        self.__time2release = 0\n",
    "        \n",
    "        # Количество обслуженных заявок\n",
    "        self.__clients_count = 0\n",
    "        \n",
    "        # Общее время работы узла\n",
    "        self.__total_worktime = 0\n",
    "        \n",
    "        # Расчитывает среднее время на обслуживание одного клиента\n",
    "        loc_busy = self.t / self.m \n",
    "        # Список времени работы узла при занятии клиентов\n",
    "        self.__node_busy_times = list(stats.expon.rvs(loc=loc_busy, scale=loc_busy*self.exp_scale_procent, size=100000))\n",
    "        \n",
    "    @property\n",
    "    def time2release(self):\n",
    "        \"\"\" Время, через которое узел будет освобожден\n",
    "        после принятия заявки.\n",
    "        \"\"\"\n",
    "        return self.__time2release\n",
    "    \n",
    "    @property\n",
    "    def node_busy_times(self):\n",
    "        \"\"\"Список времени работы узла при занятии клиентов\"\"\"\n",
    "        return self.__node_busy_times\n",
    "    \n",
    "    @property\n",
    "    def state(self):\n",
    "        \"\"\"Состояние узла\"\"\"\n",
    "        return self.__state\n",
    "    \n",
    "    @property\n",
    "    def clients_count(self):\n",
    "        \"\"\"Количество обслуженных клиентов\"\"\"\n",
    "        return self.__clients_count\n",
    "    \n",
    "    @property\n",
    "    def total_worktime(self):\n",
    "        \"\"\"Общее время работы\"\"\"\n",
    "        return self.__total_worktime\n",
    "        \n",
    "    def take(self):\n",
    "        \"\"\"Занять узел обслуживания\"\"\"\n",
    "        \n",
    "        assert self.__state == NodeState.FREE, \"Узел уже занят\"\n",
    "        \n",
    "        # Меняем статус узла на занятый\n",
    "        self.__state = NodeState.BUSY\n",
    "        \n",
    "        # При каждом взятии заявки в работу поулчаем случайное число времени обслуживания\n",
    "        self.__time2release = self.__node_busy_times.pop()\n",
    "        \n",
    "        # Увеличиваем счетчик обслуженных клиентов\n",
    "        self.__clients_count += 1\n",
    "        \n",
    "    def release(self):\n",
    "        \"\"\"Освободить узел обслуживания\"\"\"\n",
    "        # Меняем статус узла на свободный\n",
    "        self.__state = NodeState.FREE\n",
    "        \n",
    "        # Учитываем время работы\n",
    "        self.__total_worktime += self.__time2release\n",
    "        \n",
    "        # Сбрасываем время до окончания обслуживания клиента\n",
    "        self.__time2release = 0\n",
    "        \n",
    "    def time_step(self, time):\n",
    "        \"\"\" Временной шаг для узла обслуживания.\n",
    "        Используется, если узел занят.\n",
    "        Из свойства time2release вычитается значение time.\n",
    "        Если time2release <= 0, то происходит операция освобождения узла \n",
    "        \n",
    "        Параметры\n",
    "        ---------\n",
    "        time : float\n",
    "          Время\n",
    "        \"\"\"\n",
    "        if self.__state == NodeState.FREE:\n",
    "            return\n",
    "        \n",
    "        if time >= self.__time2release:\n",
    "            self.__total_worktime += self.__time2release\n",
    "            self.__time2release = 0\n",
    "        else:\n",
    "            self.__time2release -= time\n",
    "            self.__total_worktime += time\n",
    "        \n",
    "        if self.__time2release < 1e-12:\n",
    "            self.release()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "3814dae1",
   "metadata": {},
   "outputs": [],
   "source": [
    "class BarbershopPoissonProcess:\n",
    "    \"\"\" Класс для эмуляции Пуассоновского процесса в парикмахерской\n",
    "    \n",
    "    Параметры\n",
    "    ---------\n",
    "    lambda_ : float\n",
    "      Интенсивность входящего потока клиентов.\n",
    "    m : float или [float]\n",
    "      Интенсивность обслуживания отдельного парикмахера. Если float,\n",
    "      то будет одинаковым для всех узлов обслуживания.\n",
    "      Если список, то для каждого узла индивидуальное значение.\n",
    "      В случае списка его длина должна быть равна node_count\n",
    "    t_client : int\n",
    "      Промежуток времени для интенсивности потока клиентах, указывается в минутах.\n",
    "    t_barbershop : int\n",
    "      Промежуток времени для интенсивности обслуживания клиентов каждого парикмахера, указывается в минутах.\n",
    "    node_count : int\n",
    "      Количество узлов обслуживания.\n",
    "    client_exp_scale_procent : float\n",
    "      Стандартное отклонение для генерации случайного значения времени ожидания следующего клиента.\n",
    "      По умолчанию 0.3\n",
    "    barbershop_exp_scale_procent : float\n",
    "      Стандартное отклонение для генерации случайного значения времени обслуживания клиента.\n",
    "      По умолчанию 0.3\n",
    "    start_hour : int\n",
    "      Час начала работы парикмахерской\n",
    "    end_hour : int\n",
    "      Час окончания работы парикмахерской\n",
    "    no_print : bool\n",
    "      Запрещаем печать инфомрации в консоль при симуляции\n",
    "    \"\"\"\n",
    "    \n",
    "    def __init__(self, lambda_, m, t_client, t_barbershop, node_count=3, client_exp_scale_procent=0.3, \n",
    "                 barbershop_exp_scale_procent=0.3, start_hour=9, end_hour=18, no_print=False):\n",
    "        \n",
    "        assert 0 <= client_exp_scale_procent <= 1\n",
    "        assert 0 <= barbershop_exp_scale_procent <= 1\n",
    "        assert 0 <= start_hour <= 23\n",
    "        assert 0 <= end_hour <= 23\n",
    "        assert start_hour < end_hour\n",
    "        \n",
    "        self.lambda_ = lambda_\n",
    "        self.t_client = t_client\n",
    "        self.client_exp_scale_procent = client_exp_scale_procent\n",
    "        self.no_print = no_print        \n",
    "        \n",
    "        if hasattr(m, '__iter__'):\n",
    "            assert len(m) == node_count, \"Кол-во значений интенсивности обслуживания должно быть равно кол-ву узлов\"\n",
    "            self.__barbers = [PoissonServiceNode(m=m_barber, t=t_barbershop, exp_scale_procent=barbershop_exp_scale_procent)\n",
    "                              for m_barber in m]\n",
    "        else:\n",
    "            self.__barbers = [PoissonServiceNode(m=m, t=t_barbershop, exp_scale_procent=barbershop_exp_scale_procent)\n",
    "                              for idx in range(node_count)]\n",
    "        \n",
    "        # Время начала работы заведения\n",
    "        self.__start_worktime = datetime.strptime(f\"{start_hour}:00\", \"%H:%M\")\n",
    "        # Время окончания работы заведения\n",
    "        self.__end_worktime = datetime.strptime(f\"{end_hour}:00\", \"%H:%M\")\n",
    "        # Текущее время работы заведения, используется для эмуляции работы\n",
    "        self.__barbershop_current_time = self.__start_worktime\n",
    "        \n",
    "        # Очередю клиентов\n",
    "        self.__queue_client = Queue()\n",
    "        \n",
    "        # Максимальное время ожидания парикмахера клиентом\n",
    "        self.__max_barber_wait_time = 0\n",
    "        \n",
    "    def __len__(self):\n",
    "        return len(self.__barbers)\n",
    "    \n",
    "    @property\n",
    "    def max_barber_wait_time(self):\n",
    "        return self.__max_barber_wait_time \n",
    "    \n",
    "    def barbers_relax_time(self, format_='H'):\n",
    "        \"\"\"Максимальное время ожидания парикмахера со стороны клиента\"\"\"\n",
    "        \n",
    "        format_ = str(format_).strip().upper()\n",
    "        assert format_ in ('H', 'M')\n",
    "        \n",
    "        if format_ == 'H':\n",
    "            devider = 60\n",
    "        elif format_ == 'M':\n",
    "            devider = 1\n",
    "             \n",
    "        total_worktime = (self.__end_worktime - self.__start_worktime).seconds / 60\n",
    "        \n",
    "        return np.array([total_worktime/devider - barber.total_worktime/devider for barber in self.__barbers])\n",
    "        \n",
    "    def print_barbers_relax_time(self, format_='H'):\n",
    "        \"\"\" ПЕЧАТЬ\n",
    "        Максимальное время ожидания парикмахера со стороны клиента\"\"\"\n",
    "        \n",
    "        format_ = str(format_).strip().upper()\n",
    "        assert format_ in ('H', 'M')\n",
    "        \n",
    "        if format_ == 'H':\n",
    "            time_type = 'ч.'\n",
    "        elif format_ == 'M':\n",
    "            time_type = 'мин.'\n",
    "            \n",
    "        all_barbers_relax_times = self.barbers_relax_time(format_=format_)\n",
    "             \n",
    "        for idx, barber_relax_time in enumerate(all_barbers_relax_times):\n",
    "            print(f\"Парикмахер {idx+1} отдыхал {barber_relax_time} {time_type}\")\n",
    "            \n",
    "    def barbers_work_time(self, format_='H'):\n",
    "        if format_ == 'H':\n",
    "            devider = 60\n",
    "        elif format_ == 'M':\n",
    "            devider = 1\n",
    "     \n",
    "        return np.array([barber.total_worktime/devider for barber in self.__barbers])\n",
    "    \n",
    "    def print_barbers_work_time(self, format_='H'):\n",
    "        \"\"\"Максимальное время ожидания парикмахера со стороны клиента\"\"\"\n",
    "        \n",
    "        if format_ == 'H':\n",
    "            time_type = 'ч.'\n",
    "        elif format_ == 'M':\n",
    "            time_type = 'мин.'\n",
    "            \n",
    "        all_barbers_times = self.barbers_work_time(format_=format_)\n",
    "        \n",
    "        for idx, barber_time in enumerate(all_barbers_times):\n",
    "            print(f\"Парикмахер {idx+1} работал {round(barber_time, 2)} {time_type}\")\n",
    "                               \n",
    "    def _get_sorted_barbers_by_release_time(self):\n",
    "        \"\"\" Сортировка парикмахеров в порядке наибыстрейшего освобождения от обслуживания текущего клиента\n",
    "        В первых рядах будут свободные парикмахеры, а дальше те, кто быстрее всего освободится\n",
    "        \n",
    "        Результат\n",
    "        ---------\n",
    "        sorted_barbers_by_free_time : list\n",
    "          Формат [(id парикмахера, количество времени до освобождения)]\n",
    "          У свободных парикмахеров время 0.\n",
    "        \"\"\"\n",
    "        \n",
    "        sorted_barbers_by_release_time = [(idx, barber.time2release) for idx, barber in enumerate(self.__barbers)]\n",
    "        sorted_barbers_by_release_time = sorted(sorted_barbers_by_release_time, key=lambda x: x[1])\n",
    "        \n",
    "        return sorted_barbers_by_release_time\n",
    "    \n",
    "    def _get_average_service_time(self):\n",
    "        \"\"\"Найти среднее время обслуживания клиентов парикмахерами\"\"\"\n",
    "        \n",
    "        barbers_total_work_time = sum([barber.total_worktime for barber in self.__barbers])\n",
    "        barbers_total_clients = sum([barber.clients_count for barber in self.__barbers])\n",
    "        \n",
    "        if barbers_total_clients == 0:\n",
    "            return 0\n",
    "        \n",
    "        return round(barbers_total_work_time / barbers_total_clients, 2)\n",
    "    \n",
    "    def _get_count_free_barbers(self):\n",
    "        \"\"\"Поиск количества свободных парикмахеров\"\"\"\n",
    "        return len([barber for barber in self.__barbers if barber.state == NodeState.FREE])\n",
    "    \n",
    "    def _take_free_burbers(self, count):\n",
    "        \"\"\" Занять свободных парикмахеров\n",
    "        \n",
    "        Параметры\n",
    "        ---------\n",
    "        count : int\n",
    "          Какое кол-во свободных парикмахеров нужно занять.\n",
    "          Не может быть больше общего количества свободных парикмахеров.\n",
    "        \"\"\"\n",
    "        \n",
    "        sorted_barbers_by_release_time = self._get_sorted_barbers_by_release_time()\n",
    "        assert len(list(filter(lambda x: x[1] == 0, sorted_barbers_by_release_time))) >= count\n",
    "        \n",
    "        busied_burbers = []\n",
    "        for idx in range(count):\n",
    "            burber_index = sorted_barbers_by_release_time[idx][0]\n",
    "            self.__barbers[burber_index].take()\n",
    "            self._delete_client_queue(1)\n",
    "            busied_burbers.append(str(burber_index+1))\n",
    "        if len(busied_burbers) == 1:\n",
    "            self._print_status(comment=f'[ОБСЛУЖИВАНИЕ] Парикмахер {\", \".join(busied_burbers)} взял нового клиента') \n",
    "        elif len(busied_burbers) > 1:\n",
    "            self._print_status(comment=f'[ОБСЛУЖИВАНИЕ] Парикмахеры {\", \".join(busied_burbers)} взяли новых клиентов') \n",
    "            \n",
    "    def _process_client_queue(self):\n",
    "        \"\"\" Обработать очередь клиентов\n",
    "        \n",
    "        Распределяет клиентов из очереди по свободным парикмахерам\n",
    "        \n",
    "        Результат\n",
    "        ---------\n",
    "        queue_client_size : int\n",
    "          Размер клиентской очереди\n",
    "        \"\"\"\n",
    "        \n",
    "        # Если время работы парикмахерской закончилось или парикмехер не успеет обслужить клиента \n",
    "        # до закрытия заведения, то очередь не будет обслужена/ И такая очередь полностью очищается.\n",
    "        if self.__barbershop_current_time >= self.__end_worktime \\\n",
    "        or (self.__end_worktime - self.__barbershop_current_time).seconds/60  < self._get_average_service_time():\n",
    "            self.__queue_client = Queue()\n",
    "            return self.__queue_client.qsize()\n",
    "        \n",
    "        count_free_barbers = self._get_count_free_barbers()\n",
    "        queue_size = self.__queue_client.qsize()\n",
    "        \n",
    "        # Находим сколько можно занять парикмахеров, нельзя занять больше, чем свободно на текущий момент\n",
    "        how_many_takes_barbers = queue_size if queue_size <= count_free_barbers else count_free_barbers\n",
    "        \n",
    "        # Занимаем свободных парикмахеров согласно нашим нуждам и ограничениям и освобождаем очередь\n",
    "        if how_many_takes_barbers > 0:\n",
    "            self._take_free_burbers(how_many_takes_barbers)\n",
    "        \n",
    "        return self.__queue_client.qsize()\n",
    "            \n",
    "    def _add_client_queue(self, count):\n",
    "        \"\"\" Добавление клиентов в очередь\n",
    "        \n",
    "        Параметры\n",
    "        ---------\n",
    "        count : int\n",
    "          Кол-во пользователей для добавления\n",
    "        \"\"\"\n",
    "        for _ in range(count):\n",
    "            self.__queue_client.put(1)\n",
    "        self._calc_current_max_barber_wait_time()\n",
    "            \n",
    "    def _delete_client_queue(self, count):\n",
    "        \"\"\" Удаление клиентов из очереди\n",
    "        \n",
    "        Параметры\n",
    "        ---------\n",
    "        count : int\n",
    "          Кол-во пользователей для удаления\n",
    "        \"\"\"\n",
    "        for _ in range(count):\n",
    "            self.__queue_client.get()  \n",
    "            \n",
    "    def _add_client_queue_and_process(self, count):\n",
    "        \"\"\" Добавление клиентов в очередь\n",
    "        \n",
    "        Параметры\n",
    "        ---------\n",
    "        count : int\n",
    "          Кол-во пользователей для добавления\n",
    "        \"\"\"\n",
    "    \n",
    "        queue_size_before = self.__queue_client.qsize()\n",
    "        self._add_client_queue(count)\n",
    "        queue_size_before_process = self.__queue_client.qsize()\n",
    "        queue_size_after_process = self._process_client_queue()\n",
    "            \n",
    "        # Если у нас свободных парикмахеров меньше, чем требуется, то пишем лог\n",
    "        if queue_size_after_process > queue_size_before:\n",
    "            self._print_status(comment=f'[ОЧЕРЕДЬ] Очередь клиентов увеличилась на {queue_size_after_process - queue_size_before}')\n",
    "            \n",
    "        # Определяем были ли взяты хоть какие-то клиенты в работу\n",
    "        clients_are_taken = queue_size_after_process < queue_size_before_process\n",
    "            \n",
    "        return self.__queue_client.qsize(), clients_are_taken\n",
    "        \n",
    "    def _do_barbershop_time_step(self, time):\n",
    "        \"\"\" Делаем временной шал вперед для заведения и парикмахеров\n",
    "        \n",
    "        Уменьшаем время освобождения каждого парикмахера на указанное время.\n",
    "        Если время занятности становится <= 0, то делаем парикмахера свободным.\n",
    "        А также увеличиваем время работы парикмахерской на указанный шаг.\n",
    "        \"\"\"\n",
    "        \n",
    "        self.__barbershop_current_time += timedelta(minutes=time)\n",
    "        \n",
    "        for barber_idx, barber in enumerate(self.__barbers):\n",
    "            barber_state_before = barber.state\n",
    "            \n",
    "            # Если время занятости barber.time2release в рамках шага станет <= 0, то парикмахер освободится\n",
    "            barber.time_step(time) \n",
    "            barber_state_after = barber.state\n",
    "            if barber_state_after != barber_state_before:\n",
    "                self._print_status(comment=f'[ОБСЛУЖИВАНИЕ] Парикмахер {barber_idx+1} обслужил клиента')      \n",
    "            \n",
    "    def _print_status(self, comment=None):\n",
    "        \"\"\" Печатаем текущий статус заведения\n",
    "        \"\"\"\n",
    "        if self.no_print:\n",
    "            return\n",
    "        \n",
    "        barbers_status_text = \" \".join([str(int(barber.state == NodeState.BUSY)) for barber in self.__barbers])\n",
    "        text = f\"{datetime.strftime(self.__barbershop_current_time, '%H:%M')}: {barbers_status_text} / {self.__queue_client.qsize()}\"\n",
    "        text += f' # {comment}' if comment else \"\"\n",
    "        print(text)\n",
    "        \n",
    "    def _get_barbershop_current_state(self):\n",
    "        \"\"\" Текущее состояние заведения\n",
    "        \n",
    "        Результат\n",
    "        ---------\n",
    "        barbershop_current_state : tuple\n",
    "          (Кол-во свободных парикмахеров, длина очереди клиентов, кол-во обслуженных клиентов)\n",
    "        \"\"\"\n",
    "        \n",
    "        count_free_burbers = self._get_count_free_barbers()\n",
    "        client_queue_length = self.__queue_client.qsize()\n",
    "        burbers_client_counts = sum([barber.clients_count for barber in self.__barbers])\n",
    "        \n",
    "        return count_free_burbers, client_queue_length, burbers_client_counts\n",
    "    \n",
    "    def __close_barbershop(self):\n",
    "        \"\"\"Дообслуживание клиентов после закрытия заведения\n",
    "        Очередь очищается, парикмахеры заканчивают обслуживание клиентов\n",
    "        \n",
    "        \"\"\"\n",
    "        assert self.__barbershop_current_time >= self.__end_worktime\n",
    "        # Очищаем очередь\n",
    "        self.__queue_client = Queue()\n",
    "        while True:\n",
    "            sorted_barbers_by_release_time = self._get_sorted_barbers_by_release_time()\n",
    "            if all([barber_release_time == 0 for _, barber_release_time in sorted_barbers_by_release_time]):\n",
    "                break\n",
    "            sorted_busy_barbers_by_release_time = list(filter(lambda x: x[1] != 0, sorted_barbers_by_release_time))\n",
    "            first_barber_index, first_barber_release_time = sorted_busy_barbers_by_release_time[0]\n",
    "            self._do_barbershop_time_step(time=self.__barbers[first_barber_index].time2release)\n",
    "            \n",
    "    def _pop(self, list_, size=1):\n",
    "        for _ in range(size):\n",
    "            _ = list_.pop()\n",
    "            \n",
    "    def _calc_current_max_barber_wait_time(self):\n",
    "        \"\"\" Поиск максимального времени ожидания свободного парикмахера клиентом из очереди в текущий момент времени\"\"\"\n",
    "        \n",
    "        queue_size = self.__queue_client.qsize()\n",
    "        free_burbers_count = self._get_count_free_barbers()\n",
    "        total_burbers_count = len(self)\n",
    "        \n",
    "        # Если все парикмахеры могут обслужить очередь, то время ожидания равно 0\n",
    "        if free_burbers_count >= queue_size:\n",
    "            return\n",
    "        \n",
    "        if queue_size <= total_burbers_count:\n",
    "            sorted_barbers_by_release_time = self._get_sorted_barbers_by_release_time()\n",
    "            # В данном случае максимальное время ожидания - ожидание последнего клиента в очереди\n",
    "            current_max_barber_wait_time = self.__barbers[sorted_barbers_by_release_time[queue_size-1][0]].time2release\n",
    "        else:\n",
    "            level_search = queue_size // total_burbers_count\n",
    "            barbers_level_release_times = [(idx, barber.node_busy_times[-level_search]) \n",
    "                                           for idx, barber in enumerate(self.__barbers)]\n",
    "            barbers_level_release_times = sorted(barbers_level_release_times, key = lambda x: x[1])[0]\n",
    "            \n",
    "            barber_idx = barbers_level_release_times[0]\n",
    "            current_max_barber_wait_time = sum(self.__barbers[barber_idx].node_busy_times[-level_search:-1])\n",
    "            \n",
    "            \n",
    "        if current_max_barber_wait_time > self.__max_barber_wait_time:\n",
    "            self.__max_barber_wait_time = round(current_max_barber_wait_time, 2) \n",
    "            \n",
    "    def _push_queue_for_time_limit(self, time_limit):\n",
    "        \"\"\" Протолкнуть очередь клиентов к парикмахерам не более чем на указанное количество минут работы\n",
    "        \n",
    "        Параметры\n",
    "        ---------\n",
    "        time_limit : float\n",
    "          Лимит времени на который можно увеличить рабочее время парикмахерской\n",
    "          \n",
    "        Результат\n",
    "        ---------\n",
    "        cum_time_step : float\n",
    "          Количество минут, на которое шагнуло рабочее время вперед\n",
    "        \"\"\"\n",
    "        \n",
    "        cum_time_step = 0\n",
    "        \n",
    "        while True:\n",
    "            \n",
    "            # Найдем ближайшего свободного парикмахера\n",
    "            sorted_barbers_by_release_time = self._get_sorted_barbers_by_release_time()\n",
    "            # Найдем время освобождения ближайшего парикмахера\n",
    "            next_barber_wait_time = sorted_barbers_by_release_time[0][1]\n",
    "            \n",
    "            cum_time_step += next_barber_wait_time\n",
    "            \n",
    "            # Если общий шаг времени превышает лимит, то заканчиваем работу\n",
    "            if cum_time_step > time_limit:\n",
    "                cum_time_step -= next_barber_wait_time\n",
    "                return cum_time_step\n",
    "            \n",
    "            # Сделаем шаг рабочего времени для получения хотя бы одного свободного парикмахера\n",
    "            self._do_barbershop_time_step(time=next_barber_wait_time)\n",
    "            \n",
    "            # Должен быть хотя бы один свободный парикмахер\n",
    "            assert self._get_count_free_barbers() >= 1\n",
    "            \n",
    "            # У нас появился как минимум один свободный парикмахер, пробуем обслужить клиентов без добавления в очередь\n",
    "            queue_size = self._process_client_queue()\n",
    "            \n",
    "            # Если очередь стала пустая, то завершаем цикл\n",
    "            if queue_size == 0:\n",
    "                return cum_time_step\n",
    "           \n",
    "    def start_simulation(self):\n",
    "        \"\"\" Запуск симуляции работы парикмахерской\n",
    "        \"\"\"\n",
    "        \n",
    "        # Сбрасываем очередь и начальное время перед стартом\n",
    "        self.__queue_client = Queue()\n",
    "        self.__barbershop_current_time = self.__start_worktime\n",
    "        \n",
    "        # Генерируем значения времени ожидания каждого клиента\n",
    "        # На определенных этапах нам нужно будет заглядывать в значения будущих клиентов,\n",
    "        # поэтому генерируем список заранее перед циклом\n",
    "        loc_client_wait = self.t_client / self.lambda_\n",
    "        clients_wait_time = stats.expon.rvs(loc=loc_client_wait, \n",
    "                                                scale=loc_client_wait*self.client_exp_scale_procent,\n",
    "                                                size=1000000)\n",
    "        # Конвертируем в список\n",
    "        clients_wait_time = list(clients_wait_time)\n",
    "        \n",
    "        # Выполняем обслуживание клиентов до тех пор, пока не закроется заведение\n",
    "        # После закрытия очередь\n",
    "        while self.__barbershop_current_time < self.__end_worktime:\n",
    "            \n",
    "            # Получаем время прихода текущего клиента\n",
    "            current_client_wait_time = clients_wait_time.pop()\n",
    "            current_queue_size = self.__queue_client.qsize()\n",
    "            \n",
    "            # Если очередь не равна 0, то перед приходом текущего клиента\n",
    "            # нужно продвинуть очередь на время не более current_client_wait_time (врежя ожидания текущего клиента)\n",
    "            if current_queue_size != 0:\n",
    "                push_time_step = self._push_queue_for_time_limit(time_limit=current_client_wait_time)\n",
    "            else:\n",
    "                push_time_step = 0\n",
    "                \n",
    "            current_client_wait_time -= push_time_step\n",
    "            \n",
    "            # Так как в Пуассоновском поток ординарный, невозможна ситуация прихода нескольких клиентов одновременно,\n",
    "            # то при получении времени прихода следующего клиента мы можем сделать шаг рабочего времени парикмахерской\n",
    "            # и уменьшить время до освобождения парикмахеров с шагом времени текущего клиента\n",
    "            self._do_barbershop_time_step(time=current_client_wait_time) \n",
    "            # Теперь время ожидания секущего клиента равна 0, фиксируем для формальности\n",
    "            current_client_wait_time = 0\n",
    "            \n",
    "            # Добавляем текущего клиента в очередь и пробуем отдать его парикмахеру свободному парикмахеру\n",
    "            queue_size, clients_are_taken = self._add_client_queue_and_process(count=1)\n",
    "            # Если клиент взят в работу, то переходим к следующему клиенту\n",
    "            if clients_are_taken:\n",
    "                continue\n",
    "            \n",
    "            # Обслужить текущего клиента не удалось, текущий клиент сел на стульчик и хочет попасть\n",
    "            # к первому освободившемуся парикмахеру.\n",
    "            # Нам требуется сделать временной шаг до первого свободного парикмахера\n",
    "            # и отдать ему одного клиента из очереди.\n",
    "            # Но есть момент, к тому времени, как освободится первый парикмахер, могут прийти еще клиенты.\n",
    "            \n",
    "            # Отсортируем парикмахеров по времени освобождения (от наименьшего времени к большему)\n",
    "            sorted_barbers_by_release_time = self._get_sorted_barbers_by_release_time()\n",
    "            # Найдем время освобождения ближайшего парикмахера\n",
    "            next_barber_wait_time = sorted_barbers_by_release_time[0][1]\n",
    "            \n",
    "            # Теперь нам нужно проверить придут ли еще клиенты за время next_barber_wait_time\n",
    "            # Если за это время придут клиенты, то нам нужно перевести время заведения к моменту прихода\n",
    "            # добавить клиента в очередь и попробовать обслужить (для логирования)\n",
    "            \n",
    "            # Время ожидания следующих клиентов с накоплением\n",
    "            cum_next_client_wait_time = 0\n",
    "            # Счетчик пришедших клиентов во время ожидания освобождения следующего парикмахера\n",
    "            came_clients_counter = 0\n",
    "            \n",
    "            # Пробегаемся по будущим клиентам. Так как мы сейчас находимся в моменте времени,\n",
    "            # когда текущий клиент только что пришел, то в clients_wait_time будут точные времена ожидания следующих клиентов\n",
    "            # по цепочке.\n",
    "            for idx in (range(1, len(clients_wait_time))):\n",
    "                \n",
    "                # Считаем сколько ждать следующего клиента с учетом предыдущего\n",
    "                cum_next_client_wait_time += clients_wait_time[-idx]\n",
    "                \n",
    "                # Если следующий клиент приходит позже, чем освободится первый парикмахер, то останавливаем цикл\n",
    "                if cum_next_client_wait_time > next_barber_wait_time:\n",
    "                    # Вычитаем время клиента, так как нам нужно сделать шаг по времени до его прихода\n",
    "                    cum_next_client_wait_time -= clients_wait_time[-idx]\n",
    "                    break\n",
    "                \n",
    "                # Клиент пришел раньше, чем освободится первый парикмахер, делаем шаг по времени к приходу этого клиента\n",
    "                self._do_barbershop_time_step(time=clients_wait_time[-idx])\n",
    "                assert self._get_count_free_barbers() == 0, \"Ни один парикмахер не может быть свободным, сейчас {self._get_count_free_barbers()}\"\n",
    "                \n",
    "                # Добавляем его в очередь и пробуем обслужить.\n",
    "                # Свободных парикмахеров не будет, клиенты не будут обслужены, \n",
    "                # но зато мы зафиксируем в логе увеличение очереди.\n",
    "                _ = self._add_client_queue_and_process(count=1)\n",
    "                \n",
    "                # Увеличиваем счетчик таких клиентов\n",
    "                came_clients_counter += 1\n",
    "                \n",
    "            # Если бы добавили в очередь будущих клиентов, то надо удалить их из clients_wait_time\n",
    "            if came_clients_counter:\n",
    "                clients_wait_time = clients_wait_time[:-came_clients_counter]\n",
    "                \n",
    "            assert cum_next_client_wait_time <= next_barber_wait_time\n",
    "            \n",
    "            # Делаем шаг по времени работы парикмахерской ровно до того времени, как освободится первый парикмахер\n",
    "            self._do_barbershop_time_step(time=next_barber_wait_time-cum_next_client_wait_time)\n",
    "            \n",
    "            assert self._get_count_free_barbers() >= 1, f\"Должен быть свободен хотя бы 1 парикмахер, сейчас {self._get_count_free_barbers()}\"\n",
    "            \n",
    "            # У нас появился как минимум один свободный парикмахер, пробуем обслужить клиентов без добавления в очередь\n",
    "            queue_size = self._process_client_queue()\n",
    "           \n",
    "        # Ко времени закрытия заведения некоторые парикмахеры могут еще работать, \n",
    "        # завершим работу парикмахеров после закрытия парикмахерской\n",
    "        self.__close_barbershop()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bf9f735f",
   "metadata": {},
   "source": [
    "## 1 Запуск симуляции\n",
    "\n",
    "Особенности симуляции:\n",
    "- если клиент приходит после окончания работы заведения, то он обслужен не будет и в очередь не встанет;\n",
    "- если клиент пришел в рабочее время парикмахерской, но парикмахеры не успеют его обслужить до конца рабочего дня, то клиент не будет обслужен и не попадет в очередь;\n",
    "- Нумерация парикмахеров в логе начинается с 1;\n",
    "- Парикмахеры работают без перерыва на обед;\n",
    "- При распределении очереди не учитывается какой парикмахер работал больше, клиента отдаем первому освободившемуся парикмахеру.\n",
    "\n",
    "### Проверка корректности работы симуляции\n",
    "\n",
    "Првоерим корректность работы эмуляции. Для этого создадим большую очередь для двух парикмахеров.\n",
    "\n",
    "Попробуем запустить симудяцию работы парикмахерской со следющими параметрами:\n",
    "- Предполагаем, что один парикмахер в среднем обслуживает 1 клиента за 40 минут (m=1, t_barbershop=40)\n",
    "- Планируется появление в среднем 1 клиента за 5 минут (lambda_ = 1, t_client=5)\n",
    "- Работает 2 парикмахера (node_count = 2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "5e58da84",
   "metadata": {},
   "outputs": [],
   "source": [
    "config = {\n",
    "    'lambda_': 1,\n",
    "    'm': 1,\n",
    "    't_client': 5,\n",
    "    't_barbershop': 40,\n",
    "    'node_count': 2,\n",
    "    'client_exp_scale_procent': 0.3,\n",
    "    'barbershop_exp_scale_procent': 0.3,\n",
    "    'start_hour': 9,\n",
    "    'end_hour': 18,\n",
    "    'no_print': False,  \n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "283834fb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "09:06: 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "09:11: 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "09:16: 1 1 / 1 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "09:22: 1 1 / 2 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "09:28: 1 1 / 3 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "09:33: 1 1 / 4 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "09:40: 1 1 / 5 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "09:45: 1 1 / 6 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "09:52: 1 1 / 7 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "09:59: 0 1 / 7 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "09:59: 1 1 / 6 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "10:08: 1 1 / 7 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "10:13: 1 0 / 7 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "10:13: 1 1 / 6 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "10:19: 1 1 / 7 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "10:25: 1 1 / 8 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "10:33: 1 1 / 9 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "10:38: 1 1 / 10 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "10:44: 1 1 / 11 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "10:45: 0 1 / 11 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "10:45: 1 1 / 10 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "10:53: 1 0 / 10 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "10:53: 1 1 / 9 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "10:54: 1 1 / 10 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "11:01: 1 1 / 11 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "11:06: 1 1 / 12 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "11:11: 1 1 / 13 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "11:19: 1 1 / 14 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "11:24: 1 1 / 15 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "11:27: 0 1 / 15 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "11:27: 1 1 / 14 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "11:32: 1 1 / 15 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "11:39: 1 1 / 16 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "11:45: 1 1 / 17 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "11:51: 1 1 / 18 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "11:52: 1 0 / 18 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "11:52: 1 1 / 17 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "12:01: 1 1 / 18 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "12:06: 1 1 / 19 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "12:12: 0 1 / 19 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "12:12: 1 1 / 18 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "12:19: 1 1 / 19 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "12:24: 1 1 / 20 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "12:30: 1 1 / 21 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "12:36: 1 1 / 22 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "12:42: 1 1 / 23 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "12:48: 1 1 / 24 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "12:58: 1 1 / 25 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "13:04: 1 1 / 26 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "13:09: 1 1 / 27 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "13:11: 0 1 / 27 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "13:11: 1 1 / 26 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "13:18: 1 1 / 27 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "13:23: 1 1 / 28 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "13:25: 1 0 / 28 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "13:25: 1 1 / 27 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "13:32: 1 1 / 28 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "13:38: 1 1 / 29 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "13:44: 1 1 / 30 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "13:50: 1 1 / 31 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "13:56: 1 1 / 32 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "14:00: 0 1 / 32 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "14:00: 1 1 / 31 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "14:07: 1 1 / 32 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "14:12: 1 0 / 32 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "14:12: 1 1 / 31 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "14:17: 1 1 / 32 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "14:23: 1 1 / 33 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "14:29: 1 1 / 34 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "14:35: 1 1 / 35 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "14:40: 1 1 / 36 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "14:46: 1 1 / 37 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "14:55: 1 1 / 38 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "15:00: 1 1 / 39 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "15:02: 0 1 / 39 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "15:02: 1 1 / 38 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "15:06: 1 0 / 38 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "15:06: 1 1 / 37 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "15:08: 1 1 / 38 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "15:13: 1 1 / 39 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "15:20: 1 1 / 40 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "15:28: 1 1 / 41 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "15:34: 1 1 / 42 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "15:42: 1 1 / 43 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "15:44: 0 1 / 43 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "15:44: 1 1 / 42 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "15:50: 1 1 / 43 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "15:55: 1 1 / 44 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "16:01: 1 1 / 45 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "16:04: 1 0 / 45 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "16:04: 1 1 / 44 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "16:14: 1 1 / 45 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "16:20: 1 1 / 46 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "16:25: 1 1 / 47 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "16:30: 1 1 / 48 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "16:36: 1 1 / 49 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "16:42: 1 1 / 50 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "16:45: 1 0 / 50 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "16:45: 1 1 / 49 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "16:51: 1 1 / 50 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "16:56: 1 1 / 51 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "17:02: 1 1 / 52 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "17:03: 0 1 / 52 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "17:03: 1 1 / 51 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "17:08: 1 1 / 52 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "17:28: 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "17:49: 0 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n"
     ]
    }
   ],
   "source": [
    "barbershop = BarbershopPoissonProcess(**config)\n",
    "barbershop.start_simulation()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "806d6f42",
   "metadata": {},
   "source": [
    "Исходя из лога видно, что парикмахеры работают без перерыва. Как только освобождаются, сразу берут нового клиента. Парикмахеры могут быть свободны только утром, когда клиенты не пришли. И вечером, когда придется отказать всем клиентам в очереди из-за необходимости вовремя уйти с работы.\n",
    "\n",
    "Проверим статистику отдыха парикмахеров, которая считается методами set и time_step класса PoissonServiceNode. Логирование и учет времени работают раздельно, если симуляция корректна, то результаты счетчика количества минут работы каждого экземпляра PoissonServiceNode должны совпадать с инфомрацией в логе. Для удобства мы будем считать количество отдыха каждого парикмахера. Найдем обзее количество рабочих минут и вычтем из них время работы каждого парикмахера."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "a2e282c8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Парикмахер 1 отдыхал 21.954101008292696 мин.\n",
      "Парикмахер 2 отдыхал 43.0112729702156 мин.\n"
     ]
    }
   ],
   "source": [
    "barbershop.print_barbers_relax_time(format_='M')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "23fc4579",
   "metadata": {},
   "source": [
    "Как видно из результата, данные сходятся. Эти минуты отдыха приходятся на начало рабочего дня и его конец. А дальше парикмахеры работают без перерыва.\n",
    "\n",
    "Теперь проверим максимальное время ожидания клиента свободного парикмахера в такой очереди:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "d3b81f17",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Максимальное ожидание свободного парикмахера: 1266.57 мин.\n"
     ]
    }
   ],
   "source": [
    "print(\"Максимальное ожидание свободного парикмахера:\", barbershop.max_barber_wait_time, 'мин.')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eb05fe32",
   "metadata": {},
   "source": [
    "## 2 Подбор оптимальных параметров\n",
    "\n",
    "Поставим для парикмахерской следующие метрики качества:\n",
    "- клиенты не должны ожидать парикмахера более 10 минут;\n",
    "- парикмахеры должны сразу же брать клиента из очереди по факту своего освобождения (соблюдается всегда в самом принципе эмуляции);\n",
    "- парикмахеры могут отднохнуть, если очередь пуста, и если обслужили своих текущих клиентов (соблюдается всегда в самом принципе эмуляции).\n",
    "\n",
    "Пусть в парикмахерской работают 4 парикмахера (node_count = 4). И предполагаем, что каждый парикмахер в среднем обслуживает одного человека за 40 минут (m=1, t_barbershop=40). Данные параметры приближены к реальным.\n",
    "\n",
    "С помощью экспериментов были подобраны следующие оптимальные параметры интенсивности потока: 3.35 человека за 40 минут (lambda_ = 3.35, t_client = 40)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "0d24ab2e",
   "metadata": {},
   "outputs": [],
   "source": [
    "config = {\n",
    "    'lambda_': 3.35,\n",
    "    'm': 1,\n",
    "    't_client': 40,\n",
    "    't_barbershop': 40,\n",
    "    'node_count': 4,\n",
    "    'client_exp_scale_procent': 0.3,\n",
    "    'barbershop_exp_scale_procent': 0.3,\n",
    "    'start_hour': 9,\n",
    "    'end_hour': 18,\n",
    "    'no_print': False,  \n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "7b8acb15",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "09:12: 1 0 0 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "09:27: 1 1 0 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "09:44: 1 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 взял нового клиента\n",
      "10:00: 0 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "10:00: 1 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "10:13: 1 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 4 взял нового клиента\n",
      "10:45: 0 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "10:45: 0 0 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "10:45: 0 0 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 обслужил клиента\n",
      "10:45: 1 0 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "10:57: 1 1 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "11:14: 1 1 0 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 4 обслужил клиента\n",
      "11:14: 1 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 взял нового клиента\n",
      "11:34: 0 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "11:34: 1 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "11:52: 1 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 4 взял нового клиента\n",
      "12:10: 1 0 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "12:10: 1 0 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 обслужил клиента\n",
      "12:10: 1 1 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "12:22: 0 1 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "12:22: 1 1 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "12:34: 1 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 взял нового клиента\n",
      "12:48: 1 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 4 обслужил клиента\n",
      "12:48: 1 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 4 взял нового клиента\n",
      "13:02: 1 0 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "13:02: 1 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "13:17: 0 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "13:17: 0 1 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 обслужил клиента\n",
      "13:17: 1 1 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "13:31: 1 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 взял нового клиента\n",
      "13:45: 1 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 4 обслужил клиента\n",
      "13:45: 1 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 4 взял нового клиента\n",
      "14:06: 0 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "14:06: 0 0 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "14:06: 1 0 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "14:23: 1 0 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 обслужил клиента\n",
      "14:23: 1 1 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "14:35: 1 1 0 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 4 обслужил клиента\n",
      "14:35: 1 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 взял нового клиента\n",
      "14:50: 0 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "14:50: 1 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "15:02: 1 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 4 взял нового клиента\n",
      "15:20: 1 1 1 1 / 1 # [ОЧЕРЕДЬ] Очередь клиентов увеличилась на 1\n",
      "15:22: 1 0 1 1 / 1 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "15:22: 1 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "15:41: 0 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "15:41: 0 1 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 обслужил клиента\n",
      "15:41: 1 1 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "15:53: 1 1 0 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 4 обслужил клиента\n",
      "15:53: 1 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 взял нового клиента\n",
      "16:10: 1 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 4 взял нового клиента\n",
      "16:29: 0 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "16:29: 0 0 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "16:29: 1 0 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 взял нового клиента\n",
      "16:49: 1 1 1 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 взял нового клиента\n",
      "17:02: 1 1 0 1 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 обслужил клиента\n",
      "17:02: 1 1 0 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 4 обслужил клиента\n",
      "17:02: 1 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 взял нового клиента\n",
      "17:18: 0 1 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 1 обслужил клиента\n",
      "17:49: 0 0 1 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 2 обслужил клиента\n",
      "18:03: 0 0 0 0 / 0 # [ОБСЛУЖИВАНИЕ] Парикмахер 3 обслужил клиента\n"
     ]
    }
   ],
   "source": [
    "barbershop = BarbershopPoissonProcess(**config)\n",
    "barbershop.start_simulation()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6bcbf1a8",
   "metadata": {},
   "source": [
    "#### Посчитаем среднее время ожидания клиентом свободного парикмахера и среднее время отдыха парикмахеров"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "bed090f0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "f2821c29bcf6476883b51b7001c298e5",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/100 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Среднее максимальное ожидание свободного парикмахера: 10.24 мин.\n",
      "Среднее время отдыха парикмахера 1: 90.34503676790915 мин.\n",
      "Среднее время отдыха парикмахера 2: 120.8482890581213 мин.\n",
      "Среднее время отдыха парикмахера 3: 152.58210420668166 мин.\n",
      "Среднее время отдыха парикмахера 4: 225.51795021226567 мин.\n"
     ]
    }
   ],
   "source": [
    "num_experiments = 100\n",
    "max_barber_wait_time_list = []\n",
    "barbers_relax_time = np.zeros(shape=config['node_count'])\n",
    "\n",
    "config['no_print'] = True\n",
    "for _ in tqdm(range(num_experiments)):\n",
    "    barbershop = BarbershopPoissonProcess(**config)\n",
    "    barbershop.start_simulation()\n",
    "    max_barber_wait_time_list.append(barbershop.max_barber_wait_time)\n",
    "    barbers_relax_time += barbershop.barbers_relax_time(format_='M')\n",
    "    \n",
    "print(\"Среднее максимальное ожидание свободного парикмахера:\", round(np.mean(max_barber_wait_time_list),2), 'мин.')\n",
    "\n",
    "for idx, barber_relax_time in enumerate(barbers_relax_time):\n",
    "    print(f\"Среднее время отдыха парикмахера {idx+1}:\", barbers_relax_time[idx] / num_experiments, 'мин.')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "f01fd1f9",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Интенсивность потока: 3.35\n",
      "Суммарная интенсивность обслуживания: 4\n"
     ]
    }
   ],
   "source": [
    "print(\"Интенсивность потока:\", config['lambda_'])\n",
    "print(\"Суммарная интенсивность обслуживания:\", config['node_count']*config['m'])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f59f46c",
   "metadata": {},
   "source": [
    "## 3 Объяснение соотношения интенсивности потока и интенсивности обслуживания\n",
    "\n",
    "Ради эесперимента будем увеличивать интенсивность потока каждый раз в 2 раза и подбирать интенсивность обслуживания каждого парикмахера с сохранением нашей метрики времени ожидания не более 10 минут. \n",
    "\n",
    "И посмотрим как будет меняться суммарная интенсивность ожидания."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "6528be61",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "79120214e2d6460fa28fa97b773e6422",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/100 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Среднее максимальное ожидание свободного парикмахера: 10.21 мин.\n",
      "Среднее время отдыха парикмахера 1: 69.11019540281423 мин.\n",
      "Среднее время отдыха парикмахера 2: 85.57006374284012 мин.\n",
      "Среднее время отдыха парикмахера 3: 104.90269641423488 мин.\n",
      "Среднее время отдыха парикмахера 4: 142.26619199833382 мин.\n"
     ]
    }
   ],
   "source": [
    "config = {\n",
    "    'lambda_': 6.70,\n",
    "    'm': 1.8,\n",
    "    't_client': 40,\n",
    "    't_barbershop': 40,\n",
    "    'node_count': 4,\n",
    "    'client_exp_scale_procent': 0.3,\n",
    "    'barbershop_exp_scale_procent': 0.3,\n",
    "    'start_hour': 9,\n",
    "    'end_hour': 18,\n",
    "    'no_print': True,  \n",
    "}\n",
    "\n",
    "max_barber_wait_time_list = []\n",
    "barbers_relax_time = np.zeros(shape=config['node_count'])\n",
    "\n",
    "config['no_print'] = True\n",
    "for _ in tqdm(range(num_experiments)):\n",
    "    barbershop = BarbershopPoissonProcess(**config)\n",
    "    barbershop.start_simulation()\n",
    "    max_barber_wait_time_list.append(barbershop.max_barber_wait_time)\n",
    "    barbers_relax_time += barbershop.barbers_relax_time(format_='M')\n",
    "    \n",
    "print(\"Среднее максимальное ожидание свободного парикмахера:\", round(np.mean(max_barber_wait_time_list),2), 'мин.')\n",
    "\n",
    "for idx, barber_relax_time in enumerate(barbers_relax_time):\n",
    "    print(f\"Среднее время отдыха парикмахера {idx+1}:\", barbers_relax_time[idx] / num_experiments, 'мин.')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "8fb2d43e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Интенсивность потока: 6.7\n",
      "Суммарная интенсивность обслуживания: 7.2\n"
     ]
    }
   ],
   "source": [
    "print(\"Интенсивность потока:\", config['lambda_'])\n",
    "print(\"Суммарная интенсивность обслуживания:\", config['node_count']*config['m'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "90e6e272",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "05148a0b39084358820c2a972d9c1c3b",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/100 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Среднее максимальное ожидание свободного парикмахера: 10.67 мин.\n",
      "Среднее время отдыха парикмахера 1: 37.410496627103555 мин.\n",
      "Среднее время отдыха парикмахера 2: 45.31332803631072 мин.\n",
      "Среднее время отдыха парикмахера 3: 54.69967359894053 мин.\n",
      "Среднее время отдыха парикмахера 4: 64.46330242985597 мин.\n"
     ]
    }
   ],
   "source": [
    "config = {\n",
    "    'lambda_': 13.4,\n",
    "    'm': 3.05,\n",
    "    't_client': 40,\n",
    "    't_barbershop': 40,\n",
    "    'node_count': 4,\n",
    "    'client_exp_scale_procent': 0.3,\n",
    "    'barbershop_exp_scale_procent': 0.3,\n",
    "    'start_hour': 9,\n",
    "    'end_hour': 18,\n",
    "    'no_print': True,  \n",
    "}\n",
    "\n",
    "max_barber_wait_time_list = []\n",
    "barbers_relax_time = np.zeros(shape=config['node_count'])\n",
    "\n",
    "config['no_print'] = True\n",
    "for _ in tqdm(range(num_experiments)):\n",
    "    barbershop = BarbershopPoissonProcess(**config)\n",
    "    barbershop.start_simulation()\n",
    "    max_barber_wait_time_list.append(barbershop.max_barber_wait_time)\n",
    "    barbers_relax_time += barbershop.barbers_relax_time(format_='M')\n",
    "    \n",
    "print(\"Среднее максимальное ожидание свободного парикмахера:\", round(np.mean(max_barber_wait_time_list),2), 'мин.')\n",
    "\n",
    "for idx, barber_relax_time in enumerate(barbers_relax_time):\n",
    "    print(f\"Среднее время отдыха парикмахера {idx+1}:\", barbers_relax_time[idx] / num_experiments, 'мин.')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "79a279df",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Интенсивность потока: 13.4\n",
      "Суммарная интенсивность обслуживания: 12.2\n"
     ]
    }
   ],
   "source": [
    "print(\"Интенсивность потока:\", config['lambda_'])\n",
    "print(\"Суммарная интенсивность обслуживания:\", config['node_count']*config['m'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "aa918be6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "b3d77f9af4c342e1a32f409845ffd648",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/100 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Среднее максимальное ожидание свободного парикмахера: 9.86 мин.\n",
      "Среднее время отдыха парикмахера 1: 21.070654284851102 мин.\n",
      "Среднее время отдыха парикмахера 2: 25.605499484469455 мин.\n",
      "Среднее время отдыха парикмахера 3: 29.308525842572436 мин.\n",
      "Среднее время отдыха парикмахера 4: 34.96545983153082 мин.\n"
     ]
    }
   ],
   "source": [
    "config = {\n",
    "    'lambda_': 26.8,\n",
    "    'm': 5.7,\n",
    "    't_client': 40,\n",
    "    't_barbershop': 40,\n",
    "    'node_count': 4,\n",
    "    'client_exp_scale_procent': 0.3,\n",
    "    'barbershop_exp_scale_procent': 0.3,\n",
    "    'start_hour': 9,\n",
    "    'end_hour': 18,\n",
    "    'no_print': True,  \n",
    "}\n",
    "\n",
    "max_barber_wait_time_list = []\n",
    "barbers_relax_time = np.zeros(shape=config['node_count'])\n",
    "\n",
    "config['no_print'] = True\n",
    "for _ in tqdm(range(num_experiments)):\n",
    "    barbershop = BarbershopPoissonProcess(**config)\n",
    "    barbershop.start_simulation()\n",
    "    max_barber_wait_time_list.append(barbershop.max_barber_wait_time)\n",
    "    barbers_relax_time += barbershop.barbers_relax_time(format_='M')\n",
    "    \n",
    "print(\"Среднее максимальное ожидание свободного парикмахера:\", round(np.mean(max_barber_wait_time_list),2), 'мин.')\n",
    "\n",
    "for idx, barber_relax_time in enumerate(barbers_relax_time):\n",
    "    print(f\"Среднее время отдыха парикмахера {idx+1}:\", barbers_relax_time[idx] / num_experiments, 'мин.')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "0bfe828a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Интенсивность потока: 26.8\n",
      "Суммарная интенсивность обслуживания: 22.8\n"
     ]
    }
   ],
   "source": [
    "print(\"Интенсивность потока:\", config['lambda_'])\n",
    "print(\"Суммарная интенсивность обслуживания:\", config['node_count']*config['m'])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04d80b69",
   "metadata": {},
   "source": [
    "Из экспериментов можно сделать вывод, что при увеличении интенсивности потока общая интенсивность обслуживания должна увеличиться примерно в такое же количество раз."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
